﻿/*
 *	Copyright (c) 2009 Queensland University of Technology. All rights reserved.
 *	The QUT Bioinformatics Collection is open source software released under the 
 *	Microsoft Public License (Ms-PL): http://www.microsoft.com/opensource/licenses.mspx.
 */
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Linq;
using System.Xml;
using QUT.Bio.Util;
using QUT.Bio.Web;

// TODO: Move SQL queries into stored procedures.

/// <summary>
/// Utility class P, used to represent (char,string) tuples.
/// </summary>
class P : Pair<char, string> {
}

[WebService( Namespace = "http://www.mquter.qut.edu.au/bio/silvermap" )]
[WebServiceBinding( ConformsTo = WsiProfiles.BasicProfile1_1 )]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
// [System.Web.Script.Services.ScriptService]
public class MapService : System.Web.Services.WebService {

	/// <summary>
	/// The maximum number of HSPs against the primary query gene in 
	/// blast result requests.
	/// <para>We expect O(N^2) HSPs in the final result set that emerges.</para>
	/// </summary>
	private static int MaxHits = 200;

	/// <summary>
	/// The maximum number of genes to be returned in gene search.
	/// </summary>
	private static int MaxGenes = 2000;

	/// <summary>
	/// Constructor: updates MaxHits and MaxGenes according to
	/// contents of web config.
	/// <para>
	/// If an error occurs parsing these values they are left unchanged at
	/// their default values.
	/// </para>
	/// </summary>
	static MapService () {
		try {
			MaxHits = int.Parse( ConfigurationManager.AppSettings["MaxHits"] );
		}
		catch {
		}

		try {
			MaxGenes = int.Parse( ConfigurationManager.AppSettings["MaxGenes"] );
		}
		catch {
		}
	}

	/// <summary>
	/// Creates and returns a new SqlConnection object to talk to the Blast result database.
	/// </summary>
	/// <returns></returns>
	private static SqlConnection MapDBConnection () {
		return new SqlConnection( System.Configuration.ConfigurationManager.ConnectionStrings["MapDBConnection"].ConnectionString );
	}

	/// <summary>
	/// Organism: a short descriptive record for genomes.
	/// </summary>
	public class Organism {
		/// <summary>
	/// The accession-version number of the organism.
		/// </summary>
		public string Accession;

		/// <summary>
	/// The name of the organism.
		/// </summary>
		public string Description;

		/// <summary>
	/// The number of genes in the organism
		/// </summary>
		public int GeneCount;
	}

	/// <summary>
	/// The known distance types
	/// </summary>
	public enum DistanceType {
		/// <summary>
	/// Blast EValue
		/// </summary>
		EValue,

		/// <summary>
	/// Positives: the number of equivalent entries in the aligned sequences.
		/// </summary>
		Positives,

		/// <summary>
	/// Identities: the number of identical elements in the aligned sequences.
		/// </summary>
		Identities
	}

	/// <summary>
	/// Returns a list of the available sequences.
	/// </summary>
	/// <returns>a list of the available sequences</returns>
	[WebMethod]
	public Organism[] ListAvailableOrganisms () {
		List<Organism> availableOrganisms = new List<Organism>();

		using ( SqlConnection connection = MapDBConnection() ) {
			SqlCommand command = connection.CreateCommand();
			command.CommandText = "Select Organism.AccessionVersion, max(Organism.Organism) Organism, count(*) GeneCount from Organism, Gene where Gene.AccessionVersion = Organism.AccessionVersion group by Organism.AccessionVersion order by 2";
			connection.Open();
			SqlDataReader r = command.ExecuteReader();

			while ( r.Read() ) {
				availableOrganisms.Add( new Organism() {
					Accession = (string) r["AccessionVersion"],
					Description = (string) r["Organism"],
					GeneCount = (int) r["GeneCount"]
				} );
			}

			connection.Close();
		}

		return availableOrganisms.ToArray();
	}

	/// <summary>
	/// Returns a list of genes, encoded as a tab-separated flat file 
	/// of gene records.
	/// The genes match a simple query string, and are drawn from a specified 
	/// genome.
	/// <para>
	///		A query string consists of a list of tokens with optional prefix 
	///		'+' and '-' operators, which have the effect of requiring or prohibiting 
	///		the specified token.
	///	</para>
	/// </summary>
	/// <param name="accession">
	///		The accession version identifier of the source organism.
	/// </param>
	/// <param name="query">The query string to match.</param>
	/// <returns></returns>
	[WebMethod]
	public string ListGenes (
		string accession,
		string query,
		int maxGenes
	) {
		maxGenes = Math.Min( maxGenes, MaxGenes );

		if ( maxGenes == 0 ) {
			return "";
		}

		// Return a tab-separated flat file rather than "objects" to reduce xml overhead.
		string[] allPatterns = query.Split( new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries );
		StringWriter result = new StringWriter();
		ListGenesInternal( accession, allPatterns, result, maxGenes );
		return result.ToString();
	}

	/// <summary>
	/// Executes a sql query then filters the gene results to match the 
	/// supplied query string.
	/// </summary>
	/// <param name="accession"></param>
	/// <param name="allPatterns"></param>
	/// <param name="result"></param>
	private void ListGenesInternal (
		string accession,
		string[] allPatterns,
		StringWriter result,
		int maxHits
	) {
		if ( allPatterns.Length > 0 ) {
			List<string> required = new List<string>();
			List<string> optional = new List<string>();
			List<string> exclusions = new List<string>();

			foreach ( string s in allPatterns ) {
				if ( s[0] == '+' && s.Length > 1 ) {
					required.Add( s.Substring( 1 ).ToLower() );
				}
				else if ( s[0] == '-' && s.Length > 1 ) {
					exclusions.Add( s.Substring( 1 ).ToLower() );
				}
				else {
					optional.Add( s.ToLower() );
				}
			}

			#region Table looks like this...
			/*
				SELECT [Id]
			  ,[AccessionVersion]
			  ,[GeneId]
			  ,[GenbankId]
			  ,[LocusTag]
			  ,[Symbol]
			  ,[Cog]
			  ,[Translation]
			  ,[Product]
			  ,[ProteinId]
			  ,[Note]
				FROM [SilverMapBeta].[dbo].[Gene]
			 */
			#endregion

			using ( SqlConnection connection = MapDBConnection() ) {
				SqlCommand command = connection.CreateCommand();

				command.CommandText = string.Format(
					"select * from gene where AccessionVersion = @AccessionVersion order by Product, LocusTag"
				);
				command.Parameters.Add( new SqlParameter( "@AccessionVersion", accession ) );

				connection.Open();
				SqlDataReader r = command.ExecuteReader();

				while ( r.Read() ) {
					decimal id = (decimal) r["Id"];
					string locusTag = (string) r["LocusTag"];
					string symbol = (string) r["Symbol"];
					string product = (string) r["Product"];
					string gi = (string) r["GenbankId"];
					string accession_ = (string) r["AccessionVersion"];
					string cog = (string) r["Cog"];
					string translation = (string) r["Translation"];
					string note = (string) r["Note"];

					string searchString = String.Format( "{0} {1} {2} {3}", locusTag, symbol, product, gi );
					
					if ( Match( searchString, required, optional, exclusions ) ) {
						WriteGene( result, id, locusTag, symbol, product, gi, accession_, cog, translation, note );
					}
				}

				connection.Close();
			}
		}
	}

	/// <summary>
	/// Returns a list of genes, encoded as a tab-separated flat file 
	/// of gene records.
	/// The genes match a simple query string, and are drawn from a specified 
	/// genome.
	/// <para>
	///		A query string consists of a list of tokens with optional prefix 
	///		'+' and '-' operators, which have the effect of requiring or prohibiting 
	///		the specified token.
	///	</para>
	/// </summary>
	/// <param name="accessions">
	///		The accession version identifier of the source organisms to search.
	/// </param>
	/// <param name="query">The query string to match.</param>
	/// <returns></returns>
	[WebMethod]
	public string ListGenesFromMultipleSequences (
		string accessions,
		string query,
		int maxRecords
	) {
		// TODO: replace iteration over genomes with a single query.
		maxRecords = Math.Min( maxRecords, MaxGenes );

		string[] allAccessions = accessions.Split( new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries );
		string[] allPatterns = query.Split( new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries );
		StringWriter result = new StringWriter();

		if ( maxRecords < allAccessions.Length ) {
			return "";
		}

		foreach ( string accession in allAccessions ) {
			ListGenesInternal( accession, allPatterns, result, (int) ( Math.Round( (double) maxRecords / allAccessions.Length ) ) );
		}

		Debug.WriteLine( String.Format( "ListGenesFromMultipleSequences: {0} characters returned", result.GetStringBuilder().Length ) );

		return result.ToString();
	}

	/// <summary>
	/// Writes a gene record to the supplied textwriter as a tab-separated 
	/// list on a single line.
	/// </summary>
	/// <param name="result"></param>
	/// <param name="id"></param>
	/// <param name="locusTag"></param>
	/// <param name="symbol"></param>
	/// <param name="product"></param>
	/// <param name="gi"></param>
	/// <param name="accession_"></param>
	/// <param name="cog"></param>
	/// <param name="translation"></param>
	/// <param name="note"></param>
	private void WriteGene (
		TextWriter result,
		decimal id,
		string locusTag,
		string symbol,
		string product,
		string gi,
		string accession_,
		string cog,
		string translation,
		string note
	) {
		result.WriteLine( "{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}\t",
			id,
			Escape( accession_ ),
			Escape( locusTag ),
			Escape( symbol ),
			Escape( product ),
			Escape( gi ),
			Escape( cog ),
			Escape( translation ),
			Escape( note )
		);
	}

	/// <summary>
	/// Extracts a sepcified list of features from a SqlDataReader and appends the 
	/// resulting values to the supplied TextWriter as a tab-separated record on a 
	/// single line.
	/// </summary>
	/// <param name="result"></param>
	/// <param name="id"></param>
	/// <param name="features"></param>
	/// <param name="r"></param>
	private void WriteSpecificFeatures (
		TextWriter result,
		decimal id,
		string[] features,
		SqlDataReader r
	) {
		result.Write( "Id\t{0}", id );

		foreach ( string feature in features ) {
			try {
				result.Write( "\t{0}\t{1}", Escape( feature ), Escape( r[feature].ToString() ) );
			}
			catch {
			}
		}

		result.WriteLine();
	}

	/// <summary>
	///		Retrieves details of a list of genes from the database and returns them 
	///		as a list of records, written one per line, with fields separated by 
	///		tabs.
	/// </summary>
	/// <param name="geneIds">
	///		The id numbers (SilverMap specific) of the required genes.
	///	</param>
	/// <returns>
	///		A string containing the encoded records.
	///	</returns>
	[WebMethod]
	public string GetBasicGeneInfo ( 
		string geneIds 
	) {
		StringWriter result = new StringWriter();
		result.WriteLine( "Begin Genes" );

		string[] ids = geneIds.Split( ',' );

		using ( SqlConnection connection = MapDBConnection() ) {
			SqlCommand command = connection.CreateCommand();

			StringWriter w = new StringWriter();
			w.Write( "select * from gene where Id in ({0}", decimal.Parse( ids[0] ) );

			for ( int i = 1; i < ids.Length; i++ ) {
				w.Write( ",{0}", decimal.Parse( ids[i] ) );
			}

			w.Write( ") order by Product, LocusTag" );

			command.CommandText = w.ToString();

			connection.Open();
			SqlDataReader r = command.ExecuteReader();

			while ( r.Read() ) {
				decimal id = (decimal) r["Id"];
				string locusTag = (string) r["LocusTag"];
				string symbol = (string) r["Symbol"];
				string product = (string) r["Product"];
				string gi = (string) r["GenbankId"];
				string accession_ = (string) r["AccessionVersion"];
				string cog = (string) r["Cog"];
				string translation = (string) r["Translation"];
				string note = (string) r["Note"];

				WriteGene( result, id, locusTag, symbol, product, gi, accession_, cog, translation, note );
			}

			connection.Close();
		}

		result.WriteLine( "End Genes" );

		return result.ToString();
	}

	/// <summary>
	/// Gets a list containing selected feature values from a list of genes, 
	/// specified by gene ID.
	/// </summary>
	/// <param name="geneIds">A list of decimal gene IDs.</param>
	/// <param name="requestedFeatures">A list containing the names of the desired features.</param>
	/// <returns></returns>
	[WebMethod]
	public string GetSpecificGeneInfo ( 
		string geneIds, 
		string requestedFeatures 
	) {
		StringWriter result = new StringWriter();
		result.WriteLine( "Begin Records" );

		string[] ids = geneIds.Split( ',' );
		string[] features = requestedFeatures.Split( ',' );

		using ( SqlConnection connection = MapDBConnection() ) {
			SqlCommand command = connection.CreateCommand();

			StringWriter w = new StringWriter();
			w.Write( "select * from gene where Id in ({0}", decimal.Parse( ids[0] ) );

			for ( int i = 1; i < ids.Length; i++ ) {
				w.Write( ",{0}", decimal.Parse( ids[i] ) );
			}

			w.Write( ") order by Product, LocusTag" );

			command.CommandText = w.ToString();

			connection.Open();
			SqlDataReader r = command.ExecuteReader();

			while ( r.Read() ) {
				WriteSpecificFeatures( result, (decimal) r["Id"], features, r );
			}

			connection.Close();
		}

		result.WriteLine( "End Records" );

		return result.ToString();
	}

	/// <summary>
	/// Gets a list of feature values from all hits between a nominated query
	/// and hit sequence.
	/// </summary>
	/// <param name="queryId">
	///		The decimal gene id of the query sequence.
	///	</param>
	/// <param name="hitId">
	///		The decimal gene id of the hit sequence.
	///	</param>
	/// <param name="requestedFeatures">
	///		A list containing the names of the desired features.
	///	</param>
	/// <returns>
	///		A string containing a tab-delimited flat view of the resulting records.
	///	</returns>
	[WebMethod]
	public string GetSpecificBlastResults ( 
		string queryId, 
		string hitId, 
		string requestedFeatures 
	) {
		StringWriter result = new StringWriter();
		result.WriteLine( "Begin Records" );

		string[] features = requestedFeatures.Split( ',' );

		using ( SqlConnection connection = MapDBConnection() ) {
			SqlCommand command = connection.CreateCommand();
			command.CommandText = string.Format( "select * from hits where QueryId = {0} and HitId = {1}", decimal.Parse( queryId ), decimal.Parse( hitId ) );

			connection.Open();
			SqlDataReader r = command.ExecuteReader();

			while ( r.Read() ) {
				WriteSpecificFeatures( result, 0, features, r );
			}

			connection.Close();
		}

		result.WriteLine( "End Records" );

		return result.ToString();
	}

	/// <summary>
	///		Gets the best 'maxHits' hits agains the supplied query sequence.
	/// </summary>
	/// <param name="queryId">
	///		A decimal value encoding the guery sequence Id.
	/// </param>
	/// <param name="distanceType">
	/// </param>
	/// <param name="cutoff">
	/// </param>
	/// <param name="accessions">
	/// </param>
	/// <param name="maxHits">
	/// </param>
	/// <returns>
	///		A string containing a tab-delimited flat-file representation of the
	///		result set.
	/// </returns>
	[WebMethod]
	public string GetBlastResults (
		decimal queryId,
		DistanceType distanceType,
		double cutoff,
		string accessions,
		int maxHits
	) {
		maxHits = Math.Min( maxHits, MaxHits );

		Dictionary<string, string> accessionLookup = new Dictionary<string, string>();

		// Get a list of valid accessions
		using ( SqlConnection connection = MapDBConnection() ) {
			SqlCommand c = connection.CreateCommand();
			c.CommandText = "Select AccessionVersion from Organism";
			connection.Open();
			SqlDataReader r = c.ExecuteReader();

			while ( r.Read() ) {
				accessionLookup.Add( (string) r["AccessionVersion"], null );
			}

			r.Close();
			connection.Close();
		}

		// Create a quoted, comma separated list of valid accessions.
		string[] accessions_ = accessions.Split( ',' );
		StringWriter w = new StringWriter();
		bool resultsSeen = false;

		foreach ( string accession in accessions_ ) {
			if ( accessionLookup.ContainsKey( accession ) ) {
				w.Write( "{0}'{1}'", ( resultsSeen ? "," : "" ), accession );
				resultsSeen = true;
			}
		}

		if ( !resultsSeen ) {
			// this happens if we had no valid accession numbers.
			return "Begin Genes\nEnd Genes\nBegin Hits\nEnd Hits\n";
		}

		// Set up the nested query to pull out relevant hits
		string innerQuery = "";
		Guid guid = Guid.NewGuid();
		string subqueryName = "##temp" + guid.ToString().Replace( "-", "" );

		switch ( distanceType ) {
			case DistanceType.EValue:
				innerQuery = string.Format(
@"insert into {0} select top {2} h.HitId HitId, h.EValue Score  
from Hits h, Gene g 
where h.QueryId = @QueryId 
and h.EValue < @cutoff 
and g.Id = h.HitId 
and g.AccessionVersion in ({1})
order by 2 desc
", subqueryName, w, maxHits );
				break;
			default:
				cutoff = 1 - cutoff;
				innerQuery = String.Format(
@"insert into {0} select top {3} h.HitId HitId, h.{0} Score  
from Hits h, Gene g1, Gene g2 
where h.QueryId = @QueryId 
and h.QueryId = g1.Id 
and h.{1} >= len(g1.Translation) * @cutoff 
and g2.Id = h.HitId 
and g2.AccessionVersion in ({2})
order by 2 desc
", subqueryName, distanceType == DistanceType.Positives ? "Positive" : "[Identity]", w, maxHits );
				break;
		}

		string q1 = String.Format( @"
create table {0} ( HitId decimal, Score float )
{1}
select * from Gene where Id = @QueryId or Id in ( select HitId from {0} )
", subqueryName, innerQuery );

		string q2 = String.Format( @"
select * from Hits where (
	QueryId = @queryId or QueryId in ( select HitId from {0} )
) and (
	HitId = @queryId or HitId in ( select HitId from {0} )
)
", subqueryName );

		string q3 = String.Format( @"drop table {0}", subqueryName );

		StringWriter result = new StringWriter();

		Dictionary<Decimal, int> queryLength = new Dictionary<decimal, int>();
		// TODO: Fix blast table, then remove this work around.

		using ( SqlConnection connection = MapDBConnection() ) {
			connection.Open();


			SqlCommand command = connection.CreateCommand();
			command.CommandTimeout = 120;

			command.Parameters.Add( new SqlParameter( "@QueryId", queryId ) );
			command.Parameters.Add( new SqlParameter( "@cutoff", cutoff ) );

			SqlDataReader r = null;

			result.WriteLine( "Begin Genes" );

			try {
				command.CommandText = q1;
				r = command.ExecuteReader();

				while ( r.Read() ) {
					decimal id = (decimal) r["Id"];
					string locusTag = (string) r["LocusTag"];
					string symbol = (string) r["Symbol"];
					string product = (string) r["Product"];
					string gi = (string) r["GenbankId"];
					string accession_ = (string) r["AccessionVersion"];
					string cog = (string) r["Cog"];
					string translation = (string) r["Translation"];
					string note = (string) r["Note"];

					result.WriteLine(
						"{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}\t{8}",
						id,
						Escape( accession_ ),
						Escape( locusTag ),
						Escape( symbol ),
						Escape( product ),
						Escape( gi ),
						Escape( cog ),
						Escape( translation ),
						Escape( note )
					);

					queryLength[id] = translation.Length;
				}
			}
			catch ( Exception exception ) {
				Debug.WriteLine( string.Format( "Error deleting temp table:\n{0}", exception ) );
			}
			finally {
				r.Close();
			}

			result.WriteLine( "End Genes" );

			result.WriteLine( "Begin Hits" );

			try {

				command.CommandText = q2;
				r = command.ExecuteReader();

				int resultCounter = 0;

				while ( r.Read() ) {
					resultCounter++;

					#region Results look like this...
					/*
					SELECT [QueryId]
						  ,[HitId]
						  ,[Rank]
						  ,[AlignmentLength]
						  ,[BitScore]
						  ,[EValue]
						  ,[HitFrame]
						  ,[HitFrom]
						  ,[HitGaps]
						  ,[HitLength]
						  ,[HitSequence]
						  ,[HitTo]
						  ,[Identity]
						  ,[Midline]
						  ,[Positive]
						  ,[QueryFrame]
						  ,[QueryFrom]
						  ,[QuerySequence]
						  ,[QueryTo]
						  ,[Score]
					  FROM [SilverMapBeta].[dbo].[Hits]
				 */
					#endregion

					try {
						decimal queryId_		= (decimal) r["QueryId"];
						decimal hitId			= (decimal) r["HitId"];
						long	Identity		= (int) r["Identity"];
						long	Positive		= (int) r["Positive"];
						double	BitScore		= (double) r["BitScore"];
						double	EValue			= (double) r["EValue"];
						long	HitFrom			= (int) r["HitFrom"];
						long	HitTo			= (int) r["HitTo"];
						string	HitSequence		= (string) r["HitSequence"];
						long	HitLength		= (int) r["HitLength"];
						int		Score			= (int) r["Score"];
						long	QueryFrom		= (int) r["QueryFrom"];
						long	QueryTo			= (int) r["QueryTo"];
						long	QueryLength		= queryLength[queryId_]; // (long)	r["QueryLength"];
						string	QuerySequence	= (string) r["QuerySequence"];
						string	Midline			= (string) r["Midline"];

						double distance =
					distanceType == DistanceType.EValue ? (double) r["EValue"]
							: distanceType == DistanceType.Identities ? 1 - (double) Identity / queryLength[queryId_]
							: 1 - (double) Positive / queryLength[queryId_];

						result.Write( "{0}\t{1}\t{2}", queryId_, hitId, distance );
						result.Write( "\t{0}", Identity );
						result.Write( "	{0}", Positive );
						result.Write( "	{0}", BitScore );
						result.Write( "	{0}", EValue );
						result.Write( "	{0}", HitFrom );
						result.Write( "	{0}", HitTo );
						result.Write( "	{0}", HitSequence );
						result.Write( "	{0}", HitLength );
						result.Write( "	{0}", Score );
						result.Write( "	{0}", QueryFrom );
						result.Write( "	{0}", QueryTo );
						result.Write( "	{0}", QueryLength );
						result.Write( "	{0}", QuerySequence );
						result.Write( "	{0}", Midline );
						result.WriteLine();
					}
#pragma warning disable 168
					catch ( System.OutOfMemoryException outOfMemoryException ) {
						Debug.WriteLine( "Out of memory while processing hits" );
						break;
					}
#pragma warning restore 168
					catch ( Exception e ) {
						Debug.WriteLine( "Exception\n" + e );
					}
				}

				Debug.WriteLine( String.Format( "{0} hits received.", resultCounter ) );
			}
			catch ( Exception exception ) {
				Debug.WriteLine( string.Format( "Error deleting temp table:\n{0}", exception ) );
			}
			finally {
				r.Close();
			}

			result.WriteLine( "End Hits" );

			try {
				command.CommandText = q3;
				command.ExecuteNonQuery();
			}
			catch ( SqlException sqlException ) {
				Debug.WriteLine( string.Format( "Error deleting temp table:\n{0}", sqlException ) );
			}

			connection.Close();
		}

		return result.ToString();
	}

	/// <summary>
	/// Determines if a string matches a given set of optional/required/disallowed substrings.
	/// </summary>
	/// <param name="s"></param>
	/// <param name="required"></param>
	/// <param name="optional"></param>
	/// <param name="exclusions"></param>
	/// <returns></returns>

	public static bool Match ( string s, List<string> required, List<string> optional, List<string> exclusions ) {
		s = s.ToLower();

		foreach ( string t in required ) {
			if ( s.IndexOf( t ) < 0 ) {
				return false;
			}
		}

		foreach ( string t in exclusions ) {
			if ( s.IndexOf( t ) >= 0 ) {
				return false;
			}
		}

		if ( optional.Count > 0 ) {
			foreach ( string t in optional ) {
				if ( t == "*" || s.IndexOf( t ) >= 0 ) {
					return true;
				}
			}

			return false;
		}
		else {
			return true;
		}
	}

	private string Escape ( string s ) {
		P [] encodings = {
		    new P{ First = '&', Second = "&amp;" },
		    new P{ First = '\t', Second = "&t;" },
		    new P{ First = '\n', Second = "&n;" },
		    new P{ First = '\r', Second = "" }
		};

		StringBuilder b = new StringBuilder();

		foreach ( char c in s ) {
			foreach ( P p in encodings ) {
				if ( c == p.First ) {
					b.Append( p.Second );
					goto Skip;
				}
			}

			b.Append( c );
		Skip:
			;
		}

		return b.ToString();
	}

	public class BlastAlignment {
		public string Query = "n/a";
		public string MidLine = "n/a";
		public string Hit = "n/a";
	}

	[WebMethod]
	public BlastAlignment AlignOnDemand ( decimal queryId, decimal subjectId ) {
		BlastAlignment result = new BlastAlignment();

		Dictionary<decimal, string> translations = new Dictionary<decimal, string>();

		using ( SqlConnection connection = MapDBConnection() ) {
			try {
				connection.Open();

				SqlCommand command = connection.CreateCommand();

				command.Parameters.Add( new SqlParameter() {
					ParameterName = "queryId",
					Value = queryId
				} );

				command.Parameters.Add( new SqlParameter() {
					ParameterName = "subjectId",
					Value = subjectId
				} );

				command.CommandText = "select Id, Translation from Gene where id in ( @queryId, @subjectId )";

				SqlDataReader reader = command.ExecuteReader();

				while ( reader.Read() ) {
					decimal id = (decimal) reader["Id"];
					string translation = (string) reader["Translation"];

					translations[id] = translation;
				}
			}
			catch ( Exception e ) {
				Debug.WriteLine( e.ToString() );
			}
		}

		ProcessWrapper formatdb = null;
		ProcessWrapper blast = null;

		try {
			Guid g = Guid.NewGuid();

			FileInfo formatdbExe = new FileInfo( string.Format(
					"{0}\\formatdb.exe",
					System.Configuration.ConfigurationManager.AppSettings["BlastBin"]
				) );

			FileInfo blastExe = new FileInfo( string.Format(
					"{0}\\blastall.exe",
					System.Configuration.ConfigurationManager.AppSettings["BlastBin"]
				) );

			DirectoryInfo temp = new DirectoryInfo( "C:\\temp" );

			string queryFasta = string.Format( "{0}\\{1}.query.fasta", temp.FullName, g );
			string subjectFasta = string.Format( "{0}\\{1}.subject.fasta", temp.FullName, g );
			string outputXml = string.Format( "{0}\\{1}.output.xml", temp.FullName, g );

			File.WriteAllText( queryFasta, String.Format( ">{0}\n{1}", queryId, translations[queryId] ) );
			File.WriteAllText( subjectFasta, String.Format( ">{0}\n{1}", subjectId, translations[subjectId] ) );

			formatdb = new ProcessWrapper(
				formatdbExe,
				temp,
				null,
				"-i", string.Format( "{0}.subject.fasta", g )
			);

			formatdb.Start();

			if ( formatdb.WaitForExit( 1 * 60 * 1000 ) ) {
				formatdb.WaitForExit();
			}
			else {
				formatdb.Kill();
			}

			formatdb.Close();
			formatdb = null;

			blast = new ProcessWrapper(
				blastExe, temp, null,
				"-p", "blastp",
				"-i", string.Format( "{0}.query.fasta", g ),
				"-d", string.Format( "{0}.subject.fasta", g ),
				"-o", string.Format( "{0}.output.xml", g ),
				"-m", 7
			);

			blast.Start();

			if ( blast.WaitForExit( 1 * 60 * 1000 ) ) {
				blast.WaitForExit();
			}
			else {
				blast.Kill();
			}

			blast.Close();
			blast = null;

			XmlDocument doc = new XmlDocument();
			doc.XmlResolver = null;
			doc.Load( outputXml );

			XmlNodeList hspList = doc.GetElementsByTagName( "Hsp" );

			if ( hspList.Count > 0 ) {
				foreach ( XmlElement element in hspList[0].ChildNodes ) {
					if ( element.Name == "Hsp_qseq" ) {
						result.Query = element.InnerText;
					}
					else if ( element.Name == "Hsp_hseq" ) {
						result.Hit = element.InnerText;
					}
					else if ( element.Name == "Hsp_midline" ) {
						result.MidLine = element.InnerText;
					}
				}
			}

			string [] baseFiles = { queryFasta, subjectFasta, outputXml };
			string [] dbExtensions = { "pin", "psq", "phr" };

			foreach ( string fileName in baseFiles ) {
				File.Delete( fileName );
			}

			foreach ( string extension in dbExtensions ) {
				File.Delete( string.Format( "{0}.{1}", subjectFasta, extension ) );
			}
		}
		catch ( Exception e ) {
			Debug.WriteLine( e.ToString() );

			if ( formatdb != null ) {
				Debug.WriteLine( "Formatdb stdout = " + formatdb.StandardOutput );
				Debug.WriteLine( "Formatdb stderr = " + formatdb.StandardError );
			}

			if ( blast != null ) {
				Debug.WriteLine( "Formatdb stdout = " + formatdb.StandardOutput );
				Debug.WriteLine( "Formatdb stderr = " + formatdb.StandardError );
			}
		}
		finally {
			if ( formatdb != null && !formatdb.HasExited ) {
				formatdb.Kill();
				formatdb.Close();
			}

			if ( blast != null && !blast.HasExited ) {
				blast.Kill();
				blast.Close();
			}
		}

		return result;
	}
}

